package com.cellngine.server;

import java.io.ByteArrayInputStream;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;

import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;

import com.cellngine.Constants;
import com.cellngine.JO;
import com.cellngine.cellngine;

/**
 * InitialKeyPair
 * 
 * Allows easy generation and storage of the keys used to encrypt and decrypt the session-specific
 * key.
 * 
 * @author Felix Kirchmann <qwer@cellngine.com>
 */
public class InitialKeyPair
{
	private Cipher				cipher;
	private KeyPairGenerator	generator;
	private SecureRandom		random;
	private KeyPair				keyPair;
	
	private boolean				initialized	= false, generated = false;
	
	public InitialKeyPair()
	{
		initialize();
	}
	
	/**
	 * Initializes the random key pair generator and cipher used to en-/decrypt data, but doesn't
	 * generate a key yet.
	 */
	private synchronized void initialize()
	{
		if (initialized)
		{
			return;
		}
		try
		{
			cipher = Cipher.getInstance("ElGamal/None/NoPadding", "BC");
			generator = KeyPairGenerator.getInstance("ElGamal", "BC");
			random = new SecureRandom();
			initialized = true;
		}
		catch (final Exception e)
		{
			cellngine.log(e, Constants.log_type.error);
		}
	}
	
	/**
	 * Encrypts or decrypts <code>plainText</code> using the public / private key. If the key pair
	 * hasn't already been generated, it will be.
	 * 
	 * @param message
	 *            The message to which the ElGamal algorithm shall be applied.
	 * @param mode
	 *            Specifies whether to de- or encrypt, must be either
	 *            <code>Cipher.ENCRYPT_MODE</code> or <code>Cipher.ENCRYPT_MODE</code>.
	 * @return The result of the ElGamal de-/encryption.
	 * @throws InvalidKeyException
	 *             see <code>Cipher.doFinal(byte[])</code>.
	 * @throws BadPaddingException
	 *             see <code>Cipher.doFinal(byte[])</code>.
	 * @throws IllegalBlockSizeException
	 *             see <code>Cipher.doFinal(byte[])</code>.
	 * @throws UnsupportedOperationException
	 *             If mode is neither <code>Cipher.ENCRYPT_MODE</code> nor
	 *             <code>Cipher.ENCRYPT_MODE</code>.
	 */
	public byte[] crypt(final byte[] plainText, final int mode) throws InvalidKeyException,
			BadPaddingException, IllegalBlockSizeException, UnsupportedOperationException
	{
		if (!generated)
		{
			generateKeyPair();
		}
		if (mode == Cipher.ENCRYPT_MODE)
		{
			cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
		}
		else if (mode == Cipher.DECRYPT_MODE)
		{
			cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
		}
		else
		{
			throw new UnsupportedOperationException();
		}
		return cipher.doFinal(plainText);
	}
	
	/**
	 * Forces the generation of a new random public / private key pair.
	 */
	public synchronized void generateKeyPair()
	{
		initialize();
		generator.initialize(Constants.elgamal_key_bytes * 8, random);
		keyPair = generator.generateKeyPair();
		generated = true;
	}
	
	/**
	 * @return The <code>Cipher</code> used to encrypt and decrypt data.
	 */
	public Cipher getCipher()
	{
		initialize();
		return cipher;
	}
	
	/**
	 * @return The key pair. If it hasn't already been generated, it will be.
	 */
	public KeyPair getKeyPair()
	{
		if (!generated)
		{
			generateKeyPair();
		}
		return keyPair;
	}
	
	/**
	 * Uploads the public key part of this key pair to the hub. It first sends the
	 * <code>port<code> this server is running on, followed by a 0x00 byte and the x509-encoded public key.
	 * 
	 * @param hubURL
	 *            The full HTTP URL directing to the hub.
	 * @param port
	 *            The TCP port this server should be listening on.
	 * @return Whether or not the upload was successful.
	 */
	public synchronized boolean uploadPublicKey(final String hubURL, final int port)
	{
		if (!generated)
		{
			generateKeyPair();
		}
		try
		{
			final HttpClient httpclient = new DefaultHttpClient();
			final HttpPut httpPut = new HttpPut(hubURL);
			final X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyPair.getPublic()
					.getEncoded());
			final byte[] requestContent = JO.byteConcat(JO.toBytes("" + port + (char) 0),
					x509EncodedKeySpec.getEncoded());
			final InputStreamEntity requestEntity = new InputStreamEntity(new ByteArrayInputStream(
					requestContent), -1);
			httpPut.setEntity(requestEntity);
			final ResponseHandler<String> responseHandler = new BasicResponseHandler();
			httpclient.execute(httpPut, responseHandler);
		}
		catch (final Exception e)
		{
			cellngine.log(e, Constants.log_type.error);
			return false;
		}
		return true;
	}
}
